#include <cstdlib>
#include <iostream>
#include <string.h>
#include <stdexcept>
#include <sys/time.h>
#include <vector>
#include <fstream>

#include <GL/glfw.h>
#include <GL/gl.h>
#include <GL/glu.h>

// Game Headers
#include "FileReader.h"
#include "Entity.h"
#include "RenderManager.h"
#include "TimerHelper.h"
#include "BoundingBox.h"

// OpenGL Headers
#include "GLSL/GLSL_helper.h"
#include "MatrixStack/MStackHelp.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"

using namespace std;

// Window height and width
float windowWidth = 640;
float windowHeight = 480;

//flag and ID to toggle on and off the shader
int shade = 1;
int ShadeProg;

// Game Variables
long long startTime;
RenderManager rManager(0);
Entity entity;

vector<Prop> props;

bool renderHitbox = false;
float rotation = 0.0f;

// Time Variables
long long lastTime;
long long currentTime;
int frameCount;
long long lastCount;

void DisplayWindow() {
   float RunningTime = (float)((double)GetCurrentTimeMillis() - (double)startTime) / 1000.0f;
   float changeTime = (float)((double)GetCurrentTimeMillis() - lastTime) / 1000.0f;

   frameCount++;

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);		

   // Start our shader
   glUseProgram(ShadeProg);

   // Set the projection and view matrix
   rManager.SetProjectionMatrix(windowWidth, windowHeight);
   rManager.SetView(vec3(-entity.GetPosition().x, -1.0f, -5.0f), rotation);

   // Set the matrix stack to the identity
   rManager.LoadIdentity();
   rManager.SetModel();

   // Game Loop
   entity.GameLoop(changeTime);
   entity.Render(renderHitbox, changeTime);
   

   for (int i = 0; i < props.size(); i++) {
      props[i].Render(renderHitbox);
   }

   if (RunningTime - lastCount > 1) {
      vec3 pos = entity.GetPosition();
      vec3 vel = entity.GetVelocity();
      vec3 accel = entity.GetAcceleration();
      printf("FPS: %d\n", frameCount);
      printf("Position: {%.2f %.2f %.2f}\n", pos.x, pos.y, pos.z);
      printf("Velocity: {%.2f %.2f %.2f}\n", vel.x, vel.y, vel.z);
      printf("Accel: {%.2f %.2f %.2f}\n\n", accel.x, accel.y, accel.z);
      lastCount = RunningTime;
      frameCount = 0;
   }
   
   lastTime = GetCurrentTimeMillis();
}

void MainLoop() {
    bool running = true;

    // Main loop
    while(running) {
        DisplayWindow();

        // Swap buffers
        glfwSwapBuffers();

        // Was the window closed?
        if(!glfwGetWindowParam(GLFW_OPENED)) {
            running = false;
        }
    }
}

void KeyboardAction(int key, int action) {
    if (key == 'A') {
        entity.UpdateState("left", action == GLFW_PRESS);
    }
    else if (key == 'D') {
        entity.UpdateState("right", action == GLFW_PRESS);
    }
    else if (key == GLFW_KEY_SPACE) {
        entity.UpdateState("space", action == GLFW_PRESS);
    }
    else if (key == 'H' && action == GLFW_PRESS) {
        renderHitbox = !renderHitbox;
    }
    else if (key == 'Q' && action == GLFW_PRESS) {
        rotation += 15.0f;
    }
    else if (key == 'E' && action == GLFW_PRESS) {
        rotation -= 15.0f;
    }
}

/* Reshape */
void ReshapeWindow(int width, int height) {				
    windowWidth = (float)width;
    windowHeight = (float)height;
    glViewport(0, 0, (GLsizei)(width), (GLsizei)(height));
}

/*function to help load the shaders (both vertex and fragment */
int InstallShader(const GLchar *vShaderName, const GLchar *fShaderName) {
    GLuint VS; //handles to shader object
    GLuint FS; //handles to frag shader object
    GLint vCompiled, fCompiled, linked; //status of shader

    // Set the vertex and fragment shader
    VS = glCreateShader(GL_VERTEX_SHADER);
    FS = glCreateShader(GL_FRAGMENT_SHADER);

    // Load the source
    glShaderSource(VS, 1, &vShaderName, NULL);
    glShaderSource(FS, 1, &fShaderName, NULL);

    // Compile vertex shader and print log
    glCompileShader(VS);

    // Check vertex shader status
    printOpenGLError();
    glGetShaderiv(VS, GL_COMPILE_STATUS, &vCompiled);
    printShaderInfoLog(VS);

    // Compile fragment shader and print log
    glCompileShader(FS);
    
    // check fragment shader status
    printOpenGLError();
    glGetShaderiv(FS, GL_COMPILE_STATUS, &fCompiled);
    printShaderInfoLog(FS);

    if (!vCompiled) {
        printf("Error compiling vertex shader %s", vShaderName);
        return 0;
    }
    
    if (!fCompiled) {
        printf("Error compiling fragment shader %s", fShaderName);
        return 0;
    }

    //create a program object and attach the compiled shaders
    ShadeProg = glCreateProgram();
    glAttachShader(ShadeProg, VS);
    glAttachShader(ShadeProg, FS);
    glLinkProgram(ShadeProg);

    /* check shader status requires helper functions */
    printOpenGLError();
    glGetProgramiv(ShadeProg, GL_LINK_STATUS, &linked);
    printProgramInfoLog(ShadeProg);
    glUseProgram(ShadeProg);

    /* get handles to shader data */
    rManager = RenderManager(ShadeProg);

    printf("sucessfully installed shader %d\n", ShadeProg);

    return 1;
}

GLuint LoadTexture(const char* imagepath) {
    // Create one OpenGL texture
    GLuint textureID;
    glGenTextures(1, &textureID);
 
    // "Bind" the newly created texture : all future texture functions will modify this texture
    glBindTexture(GL_TEXTURE_2D, textureID);
 
    // Read the file, call glTexImage2D with the right parameters
    if (glfwLoadTexture2D(imagepath, GLFW_BUILD_MIPMAPS_BIT | GLFW_ORIGIN_UL_BIT) == GL_FALSE) {
        fprintf(stderr, "Failed to load texture <%s>\n", imagepath);
    }
 
    // Nice trilinear filtering.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glGenerateMipmap(GL_TEXTURE_2D);
 
    // Return the ID of the texture we just created
    return textureID;
}

void init(int argc, char **argv) {
    printf("START INIT\n");
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    			
    // Black Background
    glClearDepth(1.0f);	// Depth Buffer Setup
    glDepthFunc(GL_LEQUAL);	// The Type Of Depth Testing
    glEnable(GL_DEPTH_TEST);// Enable Depth Testing
    glEnable(GL_LIGHTING);
    glEnable(GL_LIGHT0);

    /* some matrix stack init */
    printf("   Loading identity\n");
    rManager.Initialize();
    rManager.LoadIdentity();
    
    GLuint texture = LoadTexture("models/waves.tga");
    
    entity.SetModelData("models/robot-idle.dae", texture, rManager);
    entity.SetModelData("models/robot-walk.dae", texture, rManager); 
    entity.SetModelData("models/robot-jump.dae", texture, rManager);  
    
    if (argc > 1) {
        props = ReadLevel(argv[1], rManager);
        
        for (int i = 0; i < props.size(); i++) {
            entity.AddToCollisions(props[i].GetMask());
        }
    }
    
    printf("END INIT\n");
}

int main(int argc, char **argv) {
    printf("Initializing glfw\n");
    if(!glfwInit()) {
        fprintf(stderr, "Failed to initialize GLFW\n");
        exit(EXIT_FAILURE);
    }

    printf("Creating Window\n");
    if(!glfwOpenWindow(windowWidth, windowHeight, 0,0,0,0, 16,0, GLFW_WINDOW)) {
        fprintf(stderr, "Failed to open GLFW window\n");
        glfwTerminate();
        exit(EXIT_FAILURE);
    }
    
    glfwSetWindowTitle("Senior Project");
    glfwEnable(GLFW_KEY_REPEAT);
    glfwSwapInterval(1);

    printf("Registering Callbacks\n");
    glfwSetWindowSizeCallback(ReshapeWindow);
    glfwSetKeyCallback(KeyboardAction);
    
    printf("Checking OpenGL version\n");
    getGLversion();

    printf("Installing shader\n");
    if (!InstallShader(textFileRead((char *)"shader.vert"), textFileRead((char *)"shader.frag"))) {
        printf("Error installing shader!\n");
        return EXIT_FAILURE;
    }

    try {
        printf("Setting Render data\n");
        init(argc, argv);
        
        // Set the time
        startTime = GetCurrentTimeMillis();
        currentTime = GetCurrentTimeMillis();

        printf("Entering main loop\n");
        MainLoop();
    }
    catch ( std::runtime_error &e ) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    
    // Terminate GLFW
    glfwTerminate();

    return EXIT_SUCCESS;
}
